package com.termux.tasker;

// Constants and functions for Tasker *extensions* to the plugin protocol
// See Also: http://tasker.dinglisch.net/plugins.html

// Release Notes

// v1.1 20140202
// added function variableNameValid()
// fixed some javadoc entries (thanks to David Stone)

// v1.2 20140211
// added ACTION_EDIT_EVENT

// v1.3 20140227
// added REQUESTED_TIMEOUT_MS_NONE, REQUESTED_TIMEOUT_MS_MAX and REQUESTED_TIMEOUT_MS_NEVER
// requestTimeoutMS(): added range check

// v1.4 20140516
// support for data pass through in REQUEST_QUERY intent
// some javadoc entries fixed (thanks again David :-))

// v1.5 20141120
// added RESULT_CODE_FAILED_PLUGIN_FIRST
// added Setting.VARNAME_ERROR_MESSAGE

// v1.6 20150213
// added Setting.getHintTimeoutMS()
// added Host.addHintTimeoutMS()

// v1.7 20160619
// null check for getCallingActivity() in hostSupportsOnFireVariableReplacement( Activity editActivity )

// v1.8 20161002
// added hostSupportsKeyEncoding(), setKeyEncoding() and Host.getKeysWithEncoding()

import java.security.SecureRandom;
import java.util.regex.Pattern;

import ohos.aafwk.ability.Ability;
import ohos.aafwk.content.Intent;
import ohos.aafwk.content.IntentParams;
import ohos.app.Context;
import ohos.bundle.BundleInfo;
import ohos.bundle.ElementName;
import ohos.bundle.IBundleManager;
import ohos.event.commonevent.CommonEventData;
import ohos.hiviewdfx.HiLog;
import ohos.hiviewdfx.HiLogLabel;
import ohos.rpc.RemoteException;
import ohos.utils.PacMap;
import ohos.utils.net.Uri;
import ohos.event.commonevent.CommonEventManager;

public class TaskerPlugin {

    private final static String TAG = "TaskerPlugin";

    static final HiLogLabel LABEL = new HiLogLabel(HiLog.LOG_APP, 0, TAG);

    private final static String BASE_KEY = "net.dinglisch.android.tasker";

    private final static String EXTRAS_PREFIX = BASE_KEY + ".extras.";

    private final static int FIRST_ON_FIRE_VARIABLES_TASKER_VERSION = 80;

    public final static String VARIABLE_PREFIX = "%";

    // when generating non-repeating integers, look this far back for repeats
    // see getPositiveNonRepeatingRandomInteger()
    private final static int RANDOM_HISTORY_SIZE = 100;

    /**
     * Action that the EditActivity for an event plugin should be launched by
     */
    public final static String ACTION_EDIT_EVENT = BASE_KEY + ".ACTION_EDIT_EVENT";

    private final static String VARIABLE_NAME_START_EXPRESSION = "[\\w&&[^_]]";
    private final static String VARIABLE_NAME_MID_EXPRESSION = "[\\w0-9]+";
    private final static String VARIABLE_NAME_END_EXPRESSION = "[\\w0-9&&[^_]]";

    public final static String VARIABLE_NAME_MAIN_PART_MATCH_EXPRESSION =
            VARIABLE_NAME_START_EXPRESSION + VARIABLE_NAME_MID_EXPRESSION + VARIABLE_NAME_END_EXPRESSION;

    public final static String VARIABLE_NAME_MATCH_EXPRESSION =
            VARIABLE_PREFIX + "+" +
                    VARIABLE_NAME_MAIN_PART_MATCH_EXPRESSION;

    private static Pattern VARIABLE_NAME_MATCH_PATTERN = null;


    private final static String EXTRA_VARIABLES_PacMap = EXTRAS_PREFIX + "VARIABLES";

    /**
     * Host capabilities, passed to plugin with edit intents
     */
    private final static String EXTRA_HOST_CAPABILITIES = EXTRAS_PREFIX + "HOST_CAPABILITIES";

    public final static int EXTRA_HOST_CAPABILITY_SETTING_RETURN_VARIABLES = 2;

    public final static int EXTRA_HOST_CAPABILITY_CONDITION_RETURN_VARIABLES = 4;

    public final static int EXTRA_HOST_CAPABILITY_SETTING_FIRE_VARIABLE_REPLACEMENT = 8;

    private final static int EXTRA_HOST_CAPABILITY_RELEVANT_VARIABLES = 16;

    public final static int EXTRA_HOST_CAPABILITY_SETTING_SYNCHRONOUS_EXECUTION = 32;

    public final static int EXTRA_HOST_CAPABILITY_REQUEST_QUERY_DATA_PASS_THROUGH = 64;

    public final static int EXTRA_HOST_CAPABILITY_ENCODING_JSON = 128;

    public final static int EXTRA_HOST_CAPABILITY_ALL =
            EXTRA_HOST_CAPABILITY_SETTING_RETURN_VARIABLES |
                    EXTRA_HOST_CAPABILITY_CONDITION_RETURN_VARIABLES |
                    EXTRA_HOST_CAPABILITY_SETTING_FIRE_VARIABLE_REPLACEMENT |
                    EXTRA_HOST_CAPABILITY_RELEVANT_VARIABLES |
                    EXTRA_HOST_CAPABILITY_SETTING_SYNCHRONOUS_EXECUTION |
                    EXTRA_HOST_CAPABILITY_REQUEST_QUERY_DATA_PASS_THROUGH |
                    EXTRA_HOST_CAPABILITY_ENCODING_JSON;

    public enum Encoding {JSON}

    private final static String PacMap_KEY_ENCODING_JSON_KEYS = BASE_KEY + ".JSON_ENCODED_KEYS";

    public static boolean hostSupportsKeyEncoding(IntentParams extrasFromHost, Encoding encoding) {
        switch (encoding) {
            case JSON:
                return hostSupports(extrasFromHost, EXTRA_HOST_CAPABILITY_ENCODING_JSON);
            default:
                return false;
        }
    }

    private final static String EXTRA_HINTS_PacMap = EXTRAS_PREFIX + "HINTS";

    private final static String PacMap_KEY_HINT_PREFIX = ".hints.";

    private final static String PacMap_KEY_HINT_TIMEOUT_MS = PacMap_KEY_HINT_PREFIX + "TIMEOUT";

    private final static String PacMap_KEY_RELEVANT_VARIABLES = BASE_KEY + ".RELEVANT_VARIABLES";

    public static boolean hostSupportsRelevantVariables(IntentParams extrasFromHost) {
        return hostSupports(extrasFromHost, EXTRA_HOST_CAPABILITY_RELEVANT_VARIABLES);
    }


    /**
     * Specifies to host which variables might be used by the plugin.
     * <p>
     * Used in EditActivity, before setResult().
     *
     * @param intentToHost  the intent being returned to the host
     * @param variableNames array of relevant variable names
     */
    public static void addRelevantVariableList(Intent intentToHost, String[] variableNames) {
        intentToHost.setParam(PacMap_KEY_RELEVANT_VARIABLES, variableNames);
    }

    /**
     * Validate a variable name.
     * <p>
     * The basic requirement for variables from a plugin is that they must be all lower-case.
     *
     * @param varName name to check
     */
    public static boolean variableNameValid(String varName) {

        boolean validFlag = false;

        if (varName == null)
        { HiLog.debug(LABEL, "variableNameValid: null name"); }
        else {
            if (VARIABLE_NAME_MATCH_PATTERN == null)
            { VARIABLE_NAME_MATCH_PATTERN = Pattern.compile(VARIABLE_NAME_MATCH_EXPRESSION, 0); }

            if (VARIABLE_NAME_MATCH_PATTERN.matcher(varName).matches()) {

                if (variableNameIsLocal(varName))
                { validFlag = true; }
                else
                { HiLog.debug(LABEL, "variableNameValid: name not local: " + varName); }
            } else
            { HiLog.debug(LABEL, "variableNameValid: invalid name: " + varName); }
        }

        return validFlag;
    }

    /**
     * Allows the plugin/host to indicate to each other a set of variables which they are referencing.
     * The host may use this to e.g. show a variable selection list in it's UI.
     * The host should use this if it previously indicated to the plugin that it supports relevant vars
     *
     * @param fromHostIntentExtras usually from getIntent().getExtras()
     * @return variableNames an array of relevant variable names
     */
    public static String[] getRelevantVariableList(IntentParams fromHostIntentExtras) {

        String[] relevantVars = (String[]) getPacMapValueSafe(fromHostIntentExtras, PacMap_KEY_RELEVANT_VARIABLES, String[].class, "getRelevantVariableList");

        if (relevantVars == null){
            relevantVars = new String[0];
        }


        return relevantVars;
    }

    /**
     * Used by: plugin QueryReceiver, FireReceiver
     * <p>
     * Add a PacMap of variable name/value pairs.
     * <p>
     * Names must be valid Tasker local variable names.
     * Values must be String, String [] or ArrayList<String>
     * Null values cause deletion of possible already-existing variables
     * A null value where the variable does not already exist results in attempted deletion
     * of any existing array indices (%arr1, %arr2 etc)
     *
     * @param resultExtras the result extras from the receiver onReceive (from a call to getResultExtras())
     * @param variables    the variables to send
     * @see Setting#hostSupportsVariableReturn(IntentParams)
     * @see #variableNameValid(String)
     */

    public static void addVariablePacMap(PacMap resultExtras, PacMap variables) {
        resultExtras.putPacMap(EXTRA_VARIABLES_PacMap, variables);
    }

    /**
     * Used by: plugin EditActivity
     * <p>
     * Specify the encoding for a set of PacMap keys.
     * <p>
     * This is completely optional and currently only necessary if using Setting#setVariableReplaceKeys
     * where the corresponding values of some of the keys specified are JSON encoded.
     */

    public static void setKeyEncoding(IntentParams resultParamsToHost, String[] keys, Encoding encoding) {
        if (Encoding.JSON.equals(encoding))
        {
            addStringArrayToPacMapAsString(
                    keys, resultParamsToHost, PacMap_KEY_ENCODING_JSON_KEYS, "setValueEncoding"
            );
        }
        else
        {
            HiLog.error(LABEL, "unknown encoding: " + encoding);
        }
    }

    // ----------------------------- SETTING PLUGIN ONLY --------------------------------- //

    public static class Setting {

        /**
         * Variable name into which a description of any error that occurred can be placed
         * for the user to process.
         * <p>
         * Should *only* be set when the BroadcastReceiver result code indicates a failure.
         * <p>
         * Note that the user needs to have configured the task to continue after failure of the plugin
         * action otherwise they will not be able to make use of the error message.
         * <p>
         * For use with #addRelevantVariableList(Intent, String[]) and #addVariablePacMap(PacMap, PacMap)
         */
        public final static String VARNAME_ERROR_MESSAGE = VARIABLE_PREFIX + "errmsg";

        private final static String PacMap_KEY_VARIABLE_REPLACE_STRINGS = EXTRAS_PREFIX + "VARIABLE_REPLACE_KEYS";

        private final static String EXTRA_REQUESTED_TIMEOUT = EXTRAS_PREFIX + "REQUESTED_TIMEOUT";

        public final static int REQUESTED_TIMEOUT_MS_NONE = 0;

        public final static int REQUESTED_TIMEOUT_MS_MAX = 3599000;

        public final static int REQUESTED_TIMEOUT_MS_NEVER = REQUESTED_TIMEOUT_MS_MAX + 1000;

        private final static String EXTRA_PLUGIN_COMPLETION_INTENT = EXTRAS_PREFIX + "COMPLETION_INTENT";

        public final static String EXTRA_RESULT_CODE = EXTRAS_PREFIX + "RESULT_CODE";

        public final static String EXTRA_CALL_SERVICE_PACKAGE = BASE_KEY + ".EXTRA_CALL_SERVICE_PACKAGE";
        public final static String EXTRA_CALL_SERVICE = BASE_KEY + ".EXTRA_CALL_SERVICE";
        public final static String EXTRA_CALL_SERVICE_FOREGROUND = BASE_KEY + ".EXTRA_CALL_SERVICE_FOREGROUND";

        public final static int RESULT_CODE_OK = -1;
        public final static int RESULT_CODE_OK_MINOR_FAILURES = 1;
        public final static int RESULT_CODE_FAILED = 2;
        public final static int RESULT_CODE_PENDING = 3;
        public final static int RESULT_CODE_UNKNOWN = 4;

        /**
         * If a plugin wants to define it's own error codes, start numbering them here.
         * The code will be placed in an error variable (%err in the case of Tasker) for
         * the user to process after the plugin action.
         */

        public final static int RESULT_CODE_FAILED_PLUGIN_FIRST = 10;

        /**
         * Used by: plugin EditActivity.
         * <p>
         * Indicates to plugin that host will replace variables in specified PacMap keys.
         * <p>
         * Replacement takes place every time the setting is fired, before the PacMap is
         * passed to the plugin FireReceiver.
         *
         * @param extrasFromHost intent extras from the intent received by the edit activity
         */
        public static boolean hostSupportsOnFireVariableReplacement(IntentParams extrasFromHost) {
            return hostSupports(extrasFromHost, EXTRA_HOST_CAPABILITY_SETTING_FIRE_VARIABLE_REPLACEMENT);
        }

        /**
         * Used by: plugin EditActivity.
         *
         * Description as above.
         *
         * This version also includes backwards compatibility with pre 4.2 Tasker versions.
         * At some point this function will be deprecated.
         *
         * @param  ability the plugin edit activity, needed to test calling Tasker version
         */


        public static boolean hostSupportsOnFireVariableReplacement( Ability ability ) {

            boolean supportedFlag = hostSupportsOnFireVariableReplacement(ability.getIntent().getParams());

            if ( ! supportedFlag ) {

                ElementName callingActivity = ability.getCallingAbility();

                if ( callingActivity == null )
                { HiLog.warn( LABEL, "hostSupportsOnFireVariableReplacement: null callingActivity, defaulting to false" ); }
                else {
                    String callerPackage = callingActivity.getBundleName();

                    // Tasker only supporteed this from 1.0.10
                    supportedFlag =
                            ( callerPackage.startsWith( BASE_KEY ) ) &&
                                    ( getPackageVersionCode( ability.getBundleManager(), callerPackage ) > FIRST_ON_FIRE_VARIABLES_TASKER_VERSION )
                    ;
                }
            }

            return supportedFlag;
        }

        public static boolean hostSupportsSynchronousExecution( IntentParams extrasFromHost ) {
            return hostSupports( extrasFromHost, EXTRA_HOST_CAPABILITY_SETTING_SYNCHRONOUS_EXECUTION );
        }

        /**
         * Request the host to wait the specified number of milliseconds before continuing.
         * Note that the host may choose to ignore the request.
         *
         * Maximum value is REQUESTED_TIMEOUT_MS_MAX.
         * Also available are REQUESTED_TIMEOUT_MS_NONE (continue immediately without waiting
         * for the plugin to finish) and REQUESTED_TIMEOUT_MS_NEVER (wait forever for
         * a result).
         *
         * Used in EditActivity, before setResult().
         *
         * @param  intentToHost the intent being returned to the host
         * @param  timeoutMS
         */

        public static void requestTimeoutMS( Intent intentToHost, int timeoutMS ) {
            if ( timeoutMS < 0 )
            { HiLog.warn( LABEL, "requestTimeoutMS: ignoring negative timeout (" + timeoutMS + ")" ); }
            else {
                if (
                        ( timeoutMS > REQUESTED_TIMEOUT_MS_MAX ) &&
                                ( timeoutMS != REQUESTED_TIMEOUT_MS_NEVER )
                        ) {
                    HiLog.warn( LABEL, "requestTimeoutMS: requested timeout " + timeoutMS + " exceeds maximum, setting to max (" + REQUESTED_TIMEOUT_MS_MAX + ")" );
                    timeoutMS = REQUESTED_TIMEOUT_MS_MAX;
                }
                intentToHost.setParam( EXTRA_REQUESTED_TIMEOUT, timeoutMS );
            }
        }

        /**
         * Used by: plugin EditActivity
         * <p>
         * Indicates to host which PacMap keys should be replaced.
         *
         * @param resultPacMapToHost the PacMap being returned to the host
         * @param listOfKeyNames     which PacMap keys to replace variables in when setting fires
         */

        public static void setVariableReplaceKeys(IntentParams resultPacMapToHost, String[] listOfKeyNames) {
            addStringArrayToPacMapAsString(
                    listOfKeyNames, resultPacMapToHost, PacMap_KEY_VARIABLE_REPLACE_STRINGS,
                    "setVariableReplaceKeys"
            );
        }

        /**
         * Used by: plugin FireReceiver
         * <p>
         * Indicates to plugin whether the host will process variables which it passes back
         *
         * @param extrasFromHost intent extras from the intent received by the FireReceiver
         */

        public static boolean hostSupportsVariableReturn(IntentParams extrasFromHost) {
            return hostSupports(extrasFromHost, EXTRA_HOST_CAPABILITY_SETTING_RETURN_VARIABLES);
        }

        /**
         * Used by: plugin FireReceiver
         * <p>
         * Tell the host that the plugin has finished execution.
         * <p>
         * This should only be used if RESULT_CODE_PENDING was returned by FireReceiver.onReceive().
         *
         * @param originalFireIntent the intent received from the host (via onReceive())
         * @param resultCode         level of success in performing the settings
         * @param vars               any variables that the plugin wants to set in the host
         */
        public static boolean signalFinish(Context context, Intent originalFireIntent, int resultCode, PacMap vars) {

            String errorPrefix = "signalFinish: ";

            boolean okFlag = false;

            String completionIntentString = (String) getExtraValueSafe(originalFireIntent, Setting.EXTRA_PLUGIN_COMPLETION_INTENT, String.class, "signalFinish");

            if (completionIntentString != null) {

                Uri completionIntentUri = null;
                try {
                    completionIntentUri = Uri.parse(completionIntentString);
                }
                // 	should only throw NullPointer but don't particularly trust it
                catch (Exception e) {
                    HiLog.warn(LABEL, errorPrefix + "couldn't parse " + completionIntentString);
                }

                if (completionIntentUri != null) {
                    try {
                        Intent completionIntent = Intent.parseUri(completionIntentString);

                        completionIntent.setParam(EXTRA_RESULT_CODE, resultCode);

                        if (vars != null){
                            completionIntent.setParam(EXTRA_VARIABLES_PacMap, vars);
                        }


                        String callServicePackage = (String) getExtraValueSafe(completionIntent, Setting.EXTRA_CALL_SERVICE_PACKAGE, String.class, "signalFinish");
                        String callService = (String) getExtraValueSafe(completionIntent, Setting.EXTRA_CALL_SERVICE, String.class, "signalFinish");
                        Boolean foreground = (Boolean) getExtraValueSafe(completionIntent, Setting.EXTRA_CALL_SERVICE_FOREGROUND, Boolean.class, "signalFinish");
                        if (callServicePackage != null && callService != null && foreground != null) {
                            completionIntent.setElement(new ElementName(callServicePackage, callService, ""));
                            context.startAbility(completionIntent, 1);
//                            }
                        } else {
                            CommonEventData eventData = new CommonEventData(completionIntent);
                            CommonEventManager.publishCommonEvent(eventData);
                        }

                        okFlag = true;
                    } catch (RemoteException e) {
                        HiLog.warn(LABEL, errorPrefix + "bad URI: " + completionIntentUri);
                    }
                }
            }

            return okFlag;
        }

        /**
         * Check for a hint on the timeout value the host is using.
         * Used by: plugin FireReceiver.
         * Requires Tasker 4.7+
         *
         * @param extrasFromHost intent extras from the intent received by the FireReceiver
         * @return timeoutMS the hosts timeout setting for the action or -1 if no hint is available.
         * @see #REQUESTED_TIMEOUT_MS_NONE, REQUESTED_TIMEOUT_MS_MAX, REQUESTED_TIMEOUT_MS_NEVER
         */
        public static int getHintTimeoutMS(IntentParams extrasFromHost) {

            int timeoutMS = -1;

            IntentParams hintsPacMap = (IntentParams) TaskerPlugin.getPacMapValueSafe(extrasFromHost, EXTRA_HINTS_PacMap, PacMap.class, "getHintTimeoutMS");

            if (hintsPacMap != null) {

                Integer val = (Integer) getPacMapValueSafe(hintsPacMap, PacMap_KEY_HINT_TIMEOUT_MS, Integer.class, "getHintTimeoutMS");

                if (val != null){
                    timeoutMS = val;
                }

            }

            return timeoutMS;
        }
    }

    // ----------------------------- CONDITION/EVENT PLUGIN ONLY --------------------------------- //

    public static class Condition {
        public final static String EXTRA_RESULT_RECEIVER = BASE_KEY + ".EXTRA_RESULT_RECEIVER";

        /**
         * Used by: plugin QueryReceiver
         * <p>
         * Indicates to plugin whether the host will process variables which it passes back
         *
         * @param extrasFromHost intent extras from the intent received by the QueryReceiver
         * @see #addVariablePacMap(PacMap, PacMap)
         */


        public static boolean hostSupportsVariableReturn(IntentParams extrasFromHost) {
            return hostSupports(extrasFromHost, EXTRA_HOST_CAPABILITY_CONDITION_RETURN_VARIABLES);
        }

    }

    // ----------------------------- EVENT PLUGIN ONLY --------------------------------- //

    public static class Event {

        public final static String PASS_THROUGH_PacMap_MESSAGE_ID_KEY = BASE_KEY + ".MESSAGE_ID";

        private final static String EXTRA_REQUEST_QUERY_PASS_THROUGH_DATA = EXTRAS_PREFIX + "PASS_THROUGH_DATA";

        /**
         * @param extrasFromHost intent extras from the intent received by the QueryReceiver
         */
//        public static boolean hostSupportsRequestQueryDataPassThrough(IntentParams extrasFromHost) {
//            return hostSupports(extrasFromHost, EXTRA_HOST_CAPABILITY_REQUEST_QUERY_DATA_PASS_THROUGH);
//        }

        /**
         * Specify a PacMap of data (probably representing whatever change happened in the condition)
         * which will be included in the QUERY_CONDITION broadcast sent by the host for each
         * event instance of the plugin.
         *
         * The minimal purpose is to enable the plugin to associate a QUERY_CONDITION to the
         * with the REQUEST_QUERY that caused it.
         *
         * Note that for security reasons it is advisable to also store a message ID with the PacMap
         * which can be compared to known IDs on receipt. The host cannot validate the source of
         * REQUEST_QUERY intents so fake data may be passed. Replay attacks are also possible.
         * addPassThroughMesssageID() can be used to add an ID if the plugin doesn't wish to add it's
         * own ID to the pass through PacMap.
         *
         * Note also that there are several situations where REQUEST_QUERY will not result in a
         * QUERY_CONDITION intent (e.g. event throttling by the host), so plugin-local data
         * indexed with a message ID needs to be timestamped and eventually timed-out.
         *
         * This function can be called multiple times, each time all keys in data will be added to
         * that of previous calls.
         *
         *
         */

        public static boolean hostSupportsRequestQueryDataPassThrough( IntentParams extrasFromHost ) {
            return hostSupports( extrasFromHost,  EXTRA_HOST_CAPABILITY_REQUEST_QUERY_DATA_PASS_THROUGH );
        }

        /**
         * Retrieve the pass through data from a QUERY_REQUEST from the host which was generated
         * by a REQUEST_QUERY from the plugin.
         * <p>
         * Note that if addPassThroughMessageID() was previously called, the data will contain an extra
         * key TaskerPlugin.Event.PASS_THOUGH_PacMap_MESSAGE_ID_KEY.
         *
         * @param queryConditionIntent QUERY_REQUEST sent from host
         * @return data previously added to the REQUEST_QUERY intent
         */
        public static IntentParams retrievePassThroughData(Intent queryConditionIntent) {
            return (IntentParams) getExtraValueSafe(
                    queryConditionIntent,
                    EXTRA_REQUEST_QUERY_PASS_THROUGH_DATA,
                    PacMap.class,
                    "retrievePassThroughData"
            );
        }

        /**
         * Add a message ID to a REQUEST_QUERY intent which will then be included in the corresponding
         * QUERY_CONDITION broadcast sent by the host for each event instance of the plugin.
         * <p>
         * The minimal purpose is to enable the plugin to associate a QUERY_CONDITION to the
         * with the REQUEST_QUERY that caused it. It also allows the message to be verified
         * by the plugin to prevent e.g. replay attacks
         *
         * @return an ID for the PacMap so it can be identified and the caller verified when it is again received by the plugin
         * @see #retrievePassThroughData(Intent)
         */


        public static int addPassThroughMessageID( Intent requestQueryIntent ) {
            IntentParams passThroughPacMap = retrieveOrCreatePassThroughPacMap( requestQueryIntent );
            int id = getPositiveNonRepeatingRandomInteger();
            passThroughPacMap.setParam( PASS_THROUGH_PacMap_MESSAGE_ID_KEY, id );
            return id;
        }

        /*
         * Retrieve the pass through data from a QUERY_REQUEST from the host which was generated
         * by a REQUEST_QUERY from the plugin.
         *
         * @param queryConditionIntent QUERY_REQUEST sent from host
         * @return the ID which was passed through by the host, or -1 if no ID was found
         * @see #hostSupportsRequestQueryDataPassThrough(PacMap)
         * @see #addPassThroughData(Intent,PacMap)
         */
        public static int retrievePassThroughMessageID(Intent queryConditionIntent) {

            int toReturn = -1;

            IntentParams passThroughData = Event.retrievePassThroughData(queryConditionIntent);

            if (passThroughData != null) {
                Integer id = (Integer) getPacMapValueSafe(
                        passThroughData,
                        PASS_THROUGH_PacMap_MESSAGE_ID_KEY,
                        Integer.class,
                        "retrievePassThroughMessageID"
                );

                if (id != null){
                    toReturn = id;
                }

            }

            return toReturn;
        }

        // internal use
        private static IntentParams retrieveOrCreatePassThroughPacMap(Intent requestQueryIntent) {

            IntentParams passThroughPacMap;

            if (requestQueryIntent.hasParameter(EXTRA_REQUEST_QUERY_PASS_THROUGH_DATA))
            { passThroughPacMap = requestQueryIntent.getParam(EXTRA_REQUEST_QUERY_PASS_THROUGH_DATA); }
            else {
                passThroughPacMap = new IntentParams();
                requestQueryIntent.setParam(EXTRA_REQUEST_QUERY_PASS_THROUGH_DATA, passThroughPacMap);
            }

            return passThroughPacMap;
        }
    }
    // ---------------------------------- HOST  ----------------------------------------- //

    public static class Host {

        /**
         * Tell the plugin what capabilities the host support. This should be called when sending
         * intents to any EditActivity, FireReceiver or QueryReceiver.
         *
         * @param toPlugin the intent we're sending
         * @return capabilities one or more of the EXTRA_HOST_CAPABILITY_XXX flags
         */
        public static Intent addCapabilities(Intent toPlugin, int capabilities) {
            return toPlugin.setParam(EXTRA_HOST_CAPABILITIES, capabilities);
        }

        /**
         * Add an intent to the fire intent before it goes to the plugin FireReceiver, which the plugin
         * can use to signal when it is finished. Only use if @code{pluginWantsSychronousExecution} is true.
         *
         * @param fireIntent fire intent going to the plugin
         * @param completionIntent intent which will signal the host that the plugin is finished.
         * Implementation is host-dependent.
         */

        public static void addCompletionIntent(Intent fireIntent, Intent completionIntent, ElementName callService, boolean foreground) {
            if (callService != null) {
                completionIntent.setParam(Setting.EXTRA_CALL_SERVICE_PACKAGE, callService.getBundleName());
                completionIntent.setParam(Setting.EXTRA_CALL_SERVICE, callService.getAbilityName());
                completionIntent.setParam(Setting.EXTRA_CALL_SERVICE_FOREGROUND, foreground);
            }
            fireIntent.setParam(
                    Setting.EXTRA_PLUGIN_COMPLETION_INTENT,
                    completionIntent.toUri()
            );
        }

        /**
         * When a setting plugin is finished, it sends the host the intent which was passed to it
         * via @code{addCompletionIntent}.
         *
         * @param completionIntent intent returned from the plugin when it finished.
         * @return resultCode measure of plugin success, defaults to UNKNOWN
         */
        public static int getSettingResultCode(Intent completionIntent) {

            Integer val = (Integer) getExtraValueSafe(completionIntent, Setting.EXTRA_RESULT_CODE, Integer.class, "getSettingResultCode");

            return (val == null) ? Setting.RESULT_CODE_UNKNOWN : val;
        }

        /**
         * Extract a PacMap of variables from an intent received from the FireReceiver. This
         * should be called if the host previously indicated to the plugin
         * that it supports setting variable return.
         *
         * @param resultExtras getResultExtras() from BroadcastReceiver:onReceive()
         * @return variables a PacMap of variable name/value pairs
         * @see #addCapabilities(Intent, int)
         */

        public static PacMap getVariablesPacMap(IntentParams resultExtras) {
            return (PacMap) getPacMapValueSafe(
                    resultExtras, EXTRA_VARIABLES_PacMap, PacMap.class, "getVariablesPacMap"
            );
        }

        /**
         * Inform a setting plugin of the timeout value the host is using.
         *
         * @param toPlugin  the intent we're sending
         * @param timeoutMS the hosts timeout setting for the action. Note that this may differ from
         *                  that which the plugin requests.
         */
        public static void addHintTimeoutMS(Intent toPlugin, int timeoutMS) {
            getHintsPacMap(toPlugin, "addHintTimeoutMS").putIntValue(PacMap_KEY_HINT_TIMEOUT_MS, timeoutMS);
        }

        private static PacMap getHintsPacMap(Intent intent, String funcName) {

            PacMap hintsPacMap = (PacMap) getExtraValueSafe(intent, EXTRA_HINTS_PacMap, PacMap.class, funcName);

            if (hintsPacMap == null) {
                hintsPacMap = new PacMap();
                intent.setParam(EXTRA_HINTS_PacMap, hintsPacMap);
            }

            return hintsPacMap;
        }

        public static boolean haveRequestedTimeout(IntentParams extrasFromPluginEditActivity) {
            return extrasFromPluginEditActivity.hasParam(Setting.EXTRA_REQUESTED_TIMEOUT);
        }

        public static int getRequestedTimeoutMS(IntentParams extrasFromPluginEditActivity) {
            return
                    (Integer) getPacMapValueSafe(
                            extrasFromPluginEditActivity, Setting.EXTRA_REQUESTED_TIMEOUT, Integer.class, "getRequestedTimeout"
                    )
                    ;
        }

        public static String[] getSettingVariableReplaceKeys(IntentParams fromPluginEditActivity) {
            return getStringArrayFromPacMapString(
                    fromPluginEditActivity, Setting.PacMap_KEY_VARIABLE_REPLACE_STRINGS,
                    "getSettingVariableReplaceKeys"
            );
        }

        public static String[] getKeysWithEncoding(IntentParams fromPluginEditActivity, Encoding encoding) {

            String[] toReturn = null;

            if (Encoding.JSON.equals(encoding))
            {
                toReturn = getStringArrayFromPacMapString(
                        fromPluginEditActivity, TaskerPlugin.PacMap_KEY_ENCODING_JSON_KEYS,
                        "getKeyEncoding:JSON"
                );
            }
            else { HiLog.warn(LABEL, "Host.getKeyEncoding: unknown encoding " + encoding); }

            return toReturn;
        }

        public static boolean haveRelevantVariables(PacMap b) {
            return b.hasKey(PacMap_KEY_RELEVANT_VARIABLES);
        }

        public static void cleanRelevantVariables(PacMap b) {
            b.remove(PacMap_KEY_RELEVANT_VARIABLES);
        }

        public static void cleanHints(PacMap extras) {
            extras.remove(TaskerPlugin.EXTRA_HINTS_PacMap);
        }

        public static void cleanRequestedTimeout(PacMap extras) {
            extras.remove(Setting.EXTRA_REQUESTED_TIMEOUT);
        }

        public static void cleanSettingReplaceVariables(PacMap b) {
            b.remove(Setting.PacMap_KEY_VARIABLE_REPLACE_STRINGS);
        }
    }

    // ---------------------------------- HELPER FUNCTIONS -------------------------------- //

    private static Object getPacMapValueSafe(IntentParams b, String key, Class<?> expectedClass, String funcName) {
        Object value = null;

        if (b != null) {
            if (b.hasParam(key)) {
                Object obj = b.getParam(key);
                if (obj == null)
                { HiLog.warn(LABEL, funcName + ": " + key + ": null value"); }
                else if (obj.getClass() != expectedClass)
                { HiLog.warn(LABEL, funcName + ": " + key + ": expected " + expectedClass.getClass().getName() + ", got " + obj.getClass().getName()); }
                else
                { value = obj; }
            }
        }
        return value;
    }

    private static Object getExtraValueSafe(Intent i, String key, Class<?> expectedClass, String funcName) {
        IntentParams params = new IntentParams();
        params.setParam("key", i);
        return (i.hasParameter(key)) ? getPacMapValueSafe(params, key, expectedClass, funcName) : null;
    }

    private static boolean hostSupports(IntentParams extrasFromHost, int capabilityFlag) {
        Integer flags = (Integer) getPacMapValueSafe(extrasFromHost, EXTRA_HOST_CAPABILITIES, Integer.class, "hostSupports");
        return
                (flags != null) &&
                        ((flags & capabilityFlag) > 0)
                ;
    }

    public static int getPackageVersionCode(IBundleManager pm, String packageName) {

        int code = -1;

        if (pm != null) {
            try {
                BundleInfo pi = pm.getBundleInfo(packageName, 0);
                if (pi != null){
                    code = pi.getVersionCode();
                }
            } catch (Exception e) {
                HiLog.error(LABEL, "getPackageVersionCode: exception getting package info");
            }
        }

        return code;
    }

    private static boolean variableNameIsLocal(String varName) {

        int digitCount = 0;
        int length = varName.length();

        for (int x = 0; x < length; x++) {
            char ch = varName.charAt(x);

            if (Character.isUpperCase(ch))
            { return false; }
            else if (Character.isDigit(ch))
            { digitCount++; }
        }

        if (digitCount == (varName.length() - 1))
        {
            return false;
        }

        return true;
    }

    private static String[] getStringArrayFromPacMapString(IntentParams params, String key, String funcName) {

        String spec = (String) getPacMapValueSafe(params, key, String.class, funcName);

        String[] toReturn = null;

        if (spec != null)
        { toReturn = spec.split(" "); }

        return toReturn;
    }

    private static void addStringArrayToPacMapAsString(String[] toAdd, IntentParams params, String key, String callerName) {

        StringBuilder builder = new StringBuilder();

        if (toAdd != null) {

            for (String keyName : toAdd) {

                if (keyName.contains(" "))
                { HiLog.warn(LABEL, callerName + ": ignoring bad keyName containing space: " + keyName); }
                else {
                    if (builder.length() > 0)
                    { builder.append(' '); }

                    builder.append(keyName);
                }

                if (builder.length() > 0)
                { params.setParam(key, builder.toString()); }
            }
        }
    }

    // state tracking for random number sequence
    private static int[] lastRandomsSeen = null;
    private static int randomInsertPointer = 0;
    private static SecureRandom sr = null;

    /**
     * Generate a sequence of secure random positive integers which is guaranteed not to repeat
     * in the last 100 calls to this function.
     *
     * @return a random positive integer
     */
    public static int getPositiveNonRepeatingRandomInteger() {

        // initialize on first call
        if (sr == null) {
            sr = new SecureRandom();
            lastRandomsSeen = new int[RANDOM_HISTORY_SIZE];

            for (int x = 0; x < lastRandomsSeen.length; x++) {
                lastRandomsSeen[x] = -1;
            }
        }

        int toReturn;
        do {
            // pick a number
            toReturn = sr.nextInt(Integer.MAX_VALUE);

            // check we havn't see it recently
            for (int seen : lastRandomsSeen) {
                if (seen == toReturn) {
                    toReturn = -1;
                    break;
                }
            }
        }
        while (toReturn == -1);

        // update history
        lastRandomsSeen[randomInsertPointer] = toReturn;
        randomInsertPointer = (randomInsertPointer + 1) % lastRandomsSeen.length;

        return toReturn;
    }

}